Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client/asset/dcr: refactor to support non-rpc wallets #1227

Merged
merged 10 commits into from
Oct 25, 2021

Conversation

itswisdomagain
Copy link
Member

Introduce a Wallet interface for actual wallet functionality to
be implemented by consumers as desired.

rpcWallet type is added to implement the new Wallet interface using
json-rpc commands over an rpc connection. This maintains the existing
state of the dcr ExchangeWallet while allowing external consumers
implement other types that do not require an rpc connection.

@chappjc
Copy link
Member

chappjc commented Oct 2, 2021

@itswisdomagain and I chatted about this and I think it's a great idea. It opens the door to other Decred wallets like https://github.com/planetdecred/godcr, maybe something mobile long-term, maybe a built-in, etc. It takes an approach to a Wallet interface similar to what was done in #1089 , and it generally seems like a clean change that doesn't affect the existing RPC wallets.

@itswisdomagain itswisdomagain force-pushed the dcr-client-asset-refactor branch from 51d3927 to faa1dce Compare October 2, 2021 23:24
Copy link
Collaborator

@martonp martonp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

var success bool
defer func() {
if !success {
w.client.Shutdown()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could just call Disconnect over here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, thought about it but wanted to maintain as much as possible what was in the codebase.

Copy link
Member

@JoeGruffins JoeGruffins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manual testing looks like everything is working.

@itswisdomagain
Copy link
Member Author

itswisdomagain commented Oct 15, 2021

The last commit 4a920ae adopts #1230's asset.Driver changes with some modifications to allow alternative wallet implementations aside from the builtin wallets being implemented by #1225 and #1230. One modification with semantic importance is CreateWallet -> PrepareWallet, to accommodate cases where some work is required before the ExchangeWallet can be used but not wallet creation either.

Here's an idea of how the new PrepareWallet method can be used to prepare a dcr ExchangeWallet that provides wallet-specific functionalities using a *decred.org/dcrwallet/v2/wallet.Wallet instance that is connected to a syncer (network backend) without having to register a separate driver:

func PrepareSpvWallet(w *wallet.Wallet, desc string) {
	spvWalletInfo := dcr.WalletInfo
	spvWalletInfo.DefaultConfigPath = ""
	spvWalletInfo.ConfigOpts = configOpts // defined elsewhere

	asset.PrepareWallet(dcr.BipID, &dcr.PrepareWalletParams{
		Wallet: &SpvWallet{ // SpvWallet implements the dcr.Wallet interface using the *wallet.Wallet instance
			Wallet: w,
			desc:   desc,
		},
		WalletInfo: spvWalletInfo,
	})
}

The above works as long as it is called before asset.LoadWallet(dcr.BipID, ...).

Thoughts @chappjc @buck54321 @martonp?

@martonp
Copy link
Collaborator

martonp commented Oct 15, 2021

@itswisdomagain I don't understand your change. The point of the CreateWallet method, which you are renaming to PrepareWallet, is to provide a seed for the wallet. I don't see how this would happen here.

@chappjc
Copy link
Member

chappjc commented Oct 15, 2021

Agree with @martonp. Have a look at how the various (*Driver).Create implementations work in PR #1230. The first thing is that this really is creation, so I think "prepare" is actually less clear. But my other question is, what does asset.CreateWalletParams not permit that requires using an interface{} instead? EDIT: I think I'm being too narrow minded regarding where the actual implementation of the wallet lives. See my following comment. Leaving the text of my original comment here however.

It also seems to me that the caller of this asset.PrepareWallet (current called asset.Create) would not want to know about types like SpvWallet at all, only certain types identified by some string or other enumerator. e.g. if params.Type != WalletTypeSPV { in that PR's impl of btc's (*Driver).Create

Say dex were trying to use a new hypothetical DCR backend that is say a dcrlibwallet-based built-in... what is insufficient about asset.CreateWalletParams for Create? A hypothetical godcr external would not use Create at all because presumably the user would have to have created their wallet externally first, just like with dcrwallet external, and just go to Open.

Note that we've recently decided that Exists will only apply to internal/seeded wallets that were made by a previous call to Create.

@chappjc
Copy link
Member

chappjc commented Oct 15, 2021

OK I think I'm looking at this too rigidly, thinking that any possible dcr.Wallet interface implementation would need to be defined in the client/asset/dcr package.

But I think you're looking at an external package that implements dcr.Wallet and passes that to the asset.Driver methods as an interface{}, and when the dcr package gets it, it would ensure it satisfies dcr.Wallet, and if so, use that when doing Open (presumably if the WalletConfig.Type agrees with something from the provided Wallet)?

So in this scenario, would it not be better to leave Create alone and add a UseWallet(interface{}) for this different approach?

I'm now aware that @buck54321 has thoughts about doing something similar, except by registering a constructor for a particular wallet type, so I'll leave this to you guys to sort out.

Copy link
Member

@buck54321 buck54321 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the overall goals of this work, but I do want to request some architectural changes.

With the changes in #1230, I don't think you need to provide a custom WalletInfo, just a custom WalletDefinition. How about a package-level method

type WalletConstructor func(cfg *asset.WalletConfig, logger dex.Logger, net dex.Network) (Wallet, error)

func RegisterCustomWallet(def *asset.WalletDefinition, constructor WalletConstructor)

It doesn't really have to be a driver method either. Just a package function.

I think that will negate the need for PrepareWallet as well.

@itswisdomagain
Copy link
Member Author

itswisdomagain commented Oct 16, 2021

would it not be better to leave Create alone and add a UseWallet(interface{}) for this different approach?

Works for me. I went with PrepareWallet to be generic, rather than add a new interface method that really only applies to one asset (for now at least). The way I think of it, CreateWallet is being introduced now because there is now a need to perform some preparatory work of sorts before using the regular Setup/Open/Load. Renaming to PrepareWallet lets individual asset implementations do their thing, whatever that is, to get the wallet ready for loading. Assets that support seeded/inbuilt wallets can use that method to receive needed wallet creation params and create the wallet, while assets like dcr can use that method to receive an object that satisfies the dcr.Wallet interface and use it for wallet functionality. One method defined in the interface, different possible implementations depending on the asset and wallet types supported. Could even modify PrepareWallet(interface{}) to PrepareWallet(type string, params interface{}).

That said, UseWallet(interface{}) will definitely work for this purpose, if that's the general preference.

EDIT:
After a little more thought, asset.UseWallet(assetID, interface{}) could be confusing, since we have a asset.OpenWallet/LoadWallet function, with both functions offering different interpretations of the term Wallet. Also, the Wallet in UseWallet is a really only an internal construct used and defined by a few assets and not an asset-generic item. I'll still prefer a PrepareWallet(interface{}) method (or some other name?), even if a separate CreateWallet method is added to create inbuilt wallets.

The idea is, some assets (dcr for now) might require some preparatory works of sort before they can be loaded. So while for most assets, it'd be sufficient to call asset.Setup/OpenWallet/LoadWallet, some assets might require a preceding call to asset.PrepareWallet to ensure that the wallet is ready to be loaded and started/connected.

@itswisdomagain
Copy link
Member Author

The point of the CreateWallet method, which you are renaming to PrepareWallet, is to provide a seed for the wallet. I don't see how this would happen here.

For seeded/inbuilt wallets, Core just calls asset.PrepareWallet instead of asset.CreateWallet, and passes in the same *asset.CreateWalletParams. The assets that support wallet creation can then convert the params provided to *CreateWalletParams and do their thing, while other assets like dcr can convert the params to dcr.Wallet and do its thing.

@itswisdomagain
Copy link
Member Author

itswisdomagain commented Oct 16, 2021

How about a package-level method

Just processing this, this could/should work, although I do not understand what purpose the WalletConstructor type would serve.

I'm thinking of the following for the dcr asset:

var wallet &rpcWallet // default too for wallet-specific functionality
var walletLoaded uint32 // atomic var, set to 1 after `NewWallet` is called to prevent modifying `wallet`.

func UseCustomWallet(wallet Wallet, def *asset.WalletDefinition) // sets the wallet var and appends to AvailableWallets

Although, I wonder the implication of adding wallet types dynamically, e.g. but can probably deal with that after the fact.

Also, would this mean each asset that want to support multiple ways of providing wallet functionality will have to do likewise? The only other alternative I see is adding new methods to the asset.Driver interface to enable wallets support new wallet types, such as is being done for the seeded/inbuilt wallets and as I attempted in my previous commit. I think I still prefer a driver.PrepareWallet method for assets to use in making magic happen.

@chappjc
Copy link
Member

chappjc commented Oct 16, 2021

The assets that support wallet creation can then convert the params provided to *CreateWalletParams and do their thing, while other assets like dcr can convert the params to dcr.Wallet and do its thing.

This is why I think these are two separate functions. One needs CreateWalletParams and one needs a Wallet/WalletConstructor (whichever is yet to be decided). Both are followed by Open/Load (whichever is yet to be decided). But just taking an interface{} is confusing.

The answer to constructor vs. wallet interface provided to RegisterCustomWallet/UseCustomWallet is: whichever most effectively separates the caller from the concerns of the underlying custom wallet implementation. With a constructor as the arg, the caller does not need to make the actual wallet, only inform the asset package what constructor is to be used to make a wallet for a particular WalletDefinition, then they can use Open/Load once they've registered that constructor. It keeps the setup workflow more standard.

@itswisdomagain
Copy link
Member Author

itswisdomagain commented Oct 16, 2021

With a constructor as the arg, the caller does not need to make the actual wallet, only inform the asset package what constructor is to be used to make a wallet for a particular WalletDefinition

Missed that the constructor is to make a Wallet not ExchangeWallet. Sounds like it would work smoothly. Will give it a go.

just taking an interface{} is confusing

Yeah, I agree. Whether that's why there should be separate methods on the Driver interface is where I'm less sure. Not to push it, but my reasoning is while both require different parameters and do different things, they share a similarity - getting the wallet ready to be loaded/used with asset.Open/Load. As I said in my last comment, other wallet setup processes could come up for any asset, would that require a new interface method each time? Perhaps, this is where asset-specific package level functions would come in handy. In any case, will cross that bridge when we get there. I do like that instead of having a new UseWallet/PrepareWallet/SetupWallet method that really only applies to dcr (for now at least), a dcr package-level function would be better.

@itswisdomagain itswisdomagain force-pushed the dcr-client-asset-refactor branch from 4a920ae to 883d934 Compare October 18, 2021 14:08
@itswisdomagain itswisdomagain marked this pull request as draft October 18, 2021 14:08
@itswisdomagain
Copy link
Member Author

itswisdomagain commented Oct 18, 2021

Now using a package-level function as suggested by @buck54321. Not using WalletDefinition yet, that comes with some wide-reaching changes, would rather have #1230 go in first, then I rebase this to use WalletDefinition.

Converted to draft for now, want to do a little cleanup before this will be ready for review again.

@itswisdomagain itswisdomagain force-pushed the dcr-client-asset-refactor branch from 883d934 to e5815dd Compare October 20, 2021 17:40
@itswisdomagain itswisdomagain marked this pull request as ready for review October 20, 2021 17:41
// default rpcWallet implementation. External consumers can use this function
// to provide alternative Wallet implementations, and must do so before an
// ExchangeWallet instance is created.
func RegisterCustomWallet(constructor WalletConstructor, info *asset.WalletInfo) {
Copy link
Member

@chappjc chappjc Oct 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, @itswisdomagain, thanks. When #1230 goes in, this can switch to WalletDefinition, as you've said, but overall this PR is looking pretty clean to me.

Introduce a Wallet interface for actual wallet functionality to
be implemented by consumers as desired.

rpcWallet type is added to implement the new Wallet interface using
json-rpc commands over an rpc connection. This maintains the existing
state of the dcr ExchangeWallet while allowing external consumers
implement other types that do not require an rpc connection.
@itswisdomagain itswisdomagain force-pushed the dcr-client-asset-refactor branch from 0312e7a to df9116e Compare October 22, 2021 09:56
Copy link
Member

@buck54321 buck54321 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple typos and questions, but looks and tests great.

Copy link
Collaborator

@martonp martonp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me.

@itswisdomagain itswisdomagain force-pushed the dcr-client-asset-refactor branch from 2a4f3be to 6a48850 Compare October 25, 2021 15:16
@itswisdomagain itswisdomagain force-pushed the dcr-client-asset-refactor branch from 6a48850 to b01778a Compare October 25, 2021 15:16
@chappjc chappjc merged commit 1c508cb into decred:master Oct 25, 2021
@itswisdomagain itswisdomagain deleted the dcr-client-asset-refactor branch October 25, 2021 16:38
@chappjc chappjc added this to the 0.4 milestone Nov 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants